var focus = ""; // bigger type
var color = d3.scaleOrdinal(d3.schemeCategory10);
var hop = 1;
var subCenter;
var nodeMap = {}; // id -> node
var nodeLinks = {}; // id -> link_list
var groupId = {} // id -> group
var totId = []; // curent subgraph ids
var filteredData;

function graph_layout(data, graph, width, height, legend) {
    d3.selectAll(graph).selectAll("*").remove();
    // The force simulation mutates links and nodes, so create a copy
    // so that re-evaluating this cell produces the same result.
    const links = data.links.map(d => ({ ...d }));
    const nodes = data.nodes.map(d => ({ ...d }));

    // Create a simulation with several forces.
    const simulation = d3.forceSimulation(nodes)
        .force("link", d3.forceLink(links).id(d => d.id))
        .force("charge", d3.forceManyBody())
        .force("center", d3.forceCenter(width / 2, height / 2))
        .on("tick", ticked);

    // Create the SVG container.
    const svg = d3.select(graph)
        .attr("width", width)
        .attr("height", height)
        .attr("viewBox", [0, 0, width, height])
        .attr("style", "max-width: auto; height: auto;");

    if (!legend) {
        svg.append("g")
            .call(g => g.append("text")
                .attr("x", width / 2)
                .attr("y", 20)
                .attr("fill", "black")
                .attr("font-size", "15px")
                .attr("text-anchor", "middle")
                .text(graph))
    }

    // Add a line for each link, and a circle for each node.
    const link = svg.append("g")
        .attr("stroke", "#999")
        .attr("stroke-opacity", 0.6)
        .selectAll()
        .data(links)
        .join("line")
        .attr("stroke-width", d => Math.sqrt(d.value));

    const node = svg.append("g")
        .attr("stroke", "#fff")
        .attr("stroke-width", 1.5)
        .selectAll()
        .data(nodes)
        .join("circle")
        .attr("r", d => d.id == subCenter ? 15 : (d.type == focus ? 10 : 5))
        .attr("fill", d => d.id != subCenter ? color(d.group) : color(9));

    node.append("title")
        .text(d => d.id);

    // Add a drag behavior.
    node.call(d3.drag()
        .on("start", dragstarted)
        .on("drag", dragged)
        .on("end", dragended));

    node.on("click", function () {
        displayDetails(d3.select(this).text(), filteredData);
    });

    node.on('mouseover', function (d, i) {
        d3.select(this).transition()
            .duration('50')
            .attr('opacity', '.5');
        svg.style('cursor', 'pointer')
    })
    node.on('mouseout', function (d, i) {
        d3.select(this).transition()
            .duration('50')
            .attr('opacity', '1');
        svg.style('cursor', 'default')
    }
    );


    // Set the position attributes of links and nodes each time the simulation ticks.
    function ticked() {
        link
            .attr("x1", d => d.source.x)
            .attr("y1", d => d.source.y)
            .attr("x2", d => d.target.x)
            .attr("y2", d => d.target.y);

        node
            .attr("cx", d => d.x)
            .attr("cy", d => d.y);
    }

    // Reheat the simulation when drag starts, and fix the subject position.
    function dragstarted(event) {
        if (!event.active) simulation.alphaTarget(0.3).restart();
        event.subject.fx = event.subject.x;
        event.subject.fy = event.subject.y;
    }

    // Update the subject (dragged node) position during drag.
    function dragged(event) {
        event.subject.fx = event.x;
        event.subject.fy = event.y;
    }

    // Restore the target alpha so the simulation cools after dragging ends.
    // Unfix the subject position now that it’s no longer being dragged.
    function dragended(event) {
        if (!event.active) simulation.alphaTarget(0);
        event.subject.fx = null;
        event.subject.fy = null;
    }

    svg.call(d3.zoom()
        // .extent([[0, 0], [648, 480]])
        // .scaleExtent([0.5, 100])
        .on('start', () => { // zoom 事件发生前 将变小手
            svg.style('cursor', 'pointer')
        })
        .on("zoom", zoomed)
        .on('end', () => {
            svg.style('cursor', 'default')
        }));
    // svg.call(d3.zoom()
    //     // .extent([[0, 0], [648, 480]])
    //     // .scaleExtent([0.5, 100])
    //     .on("zoom", zoomed).filter(e => {
    //         console.log(e);
    //         return (!e.ctrlKey || e.type === 'wheel') && e.button;
    //     }));

    function zoomed({ transform }) {
        let g = svg.selectAll('g')
        g.attr("transform", transform);
    }

    if (legend == true) {
        var selectedNodes = subCenter == null ? [] : ["curent selected"];

        d3.selectAll(".NodeType:checked").each(function (d) {
            selectedNodes.push(this.value);
        });

        // Add one dot in the legend for each name.
        svg.selectAll("mydots")
            .data(selectedNodes)
            .enter()
            .append("circle")
            .attr("cx", 30)
            .attr("cy", function (d, i) { return 40 + i * 25 }) // 100 is where the first dot appears. 25 is the distance between dots
            .attr("r", function (d) { return d == "curent selected" ? 10 : 7; })
            .style("fill", function (d) { return d == "curent selected" ? color(9) : color(groupId[d]) })

        // Add one dot in the legend for each name.
        svg.selectAll("mylabels")
            .data(selectedNodes)
            .enter()
            .append("text")
            .attr("x", 50)
            .attr("y", function (d, i) { return 40 + i * 25 }) // 100 is where the first dot appears. 25 is the distance between dots
            .style("fill", function (d) { return d == "curent selected" ? color(9) : color(groupId[d]) })
            .text(function (d) { return d })
            .attr("text-anchor", "left")
            .style("alignment-baseline", "middle")
    }



    return svg;
}

function barchart_layout(barchart_data, graph) {
    const marginTop = 30, marginRight = 40, marginBottom = 30, marginLeft = 40, width = 600, height = 250, xPadding = 0.5;
    const xRange = [marginLeft, width - marginRight], yRange = [height - marginBottom, marginTop];

    d3.selectAll(graph + " > g").remove();
    const barchartSvg = d3.select(graph)
        .attr("width", width)
        .attr("height", height)
        .attr("viewBox", [0, 0, width, height])
        .attr("style", "max-width: 100%; height: auto; height: intrinsic;");

    let xDomain = [], Y = [];
    for (const type in barchart_data) {
        xDomain.push(type);
        Y.push(barchart_data[type].length);
    }

    const yDomain = [0, Math.ceil(d3.max(Y) * 1.3)];

    const xScale = d3.scaleBand(xDomain, xRange).padding(xPadding);
    const yScale = d3.scaleLinear(yDomain, yRange);
    const xAxis = d3.axisBottom(xScale);
    const yAxis = d3.axisLeft(yScale).ticks(height / 50, "");

    barchartSvg.append("g")
        .attr("transform", `translate(${marginLeft},0)`)
        .call(yAxis)
        .call(g => g.select(".domain").remove())
        .call(g => g.selectAll(".tick line").clone()
            .attr("x2", width - marginLeft - marginRight)
            .attr("stroke-opacity", 0.1))
        .call(g => g.append("text")
            .attr("x", -marginLeft - 10 + width / 2)
            .attr("y", 20)
            .attr("fill", "black")
            .attr("font-size", "15px")
            .attr("text-anchor", "middle")
            .text("Type of nodes"));

    barchartSvg.append("g")
        .attr("transform", `translate(0,${height - marginBottom})`)
        .call(xAxis);

    const bar = barchartSvg.append("g")
        .selectAll("rect")
        .data(d3.range(xDomain.length))
        .join("rect")
        .transition().duration(1000)
        .attr("x", i => xScale(xDomain[i]))
        .attr("y", i => yScale(Y[i]))
        .attr("height", i => yScale(0) - yScale(Y[i]))
        .attr("width", xScale.bandwidth())
        .attr("fill", i => color(groupId[xDomain[i]]));

    const title = barchartSvg.append("g")
        .selectAll("text")
        .data(d3.range(xDomain.length))
        .join("text")
        .transition().duration(1500)
        .attr("x", i => xScale(xDomain[i]) + xScale.bandwidth() / 2)
        .attr("y", i => yScale(Y[i]) - 5)
        .attr("fill", "black")
        .attr("text-anchor", "middle")
        .attr("font-family", "arial")
        .text(i => Y[i].toFixed(0));

    return barchartSvg;
}

async function tsne_layout(data, graph) {
    d3.selectAll(graph + " > g").remove();
    margin = ({ top: 25, right: 20, bottom: 35, left: 40 })
    height = 300
    width = 300

    if (graph == "#tsne_layout_node") {
        result = await getEmbedNode(data);
    } else if (graph == "#tsne_layout_edge") {
        result = await getEmbedEdge(data);
    }

    if (result.status == 'error') {
        const svg = d3.select(graph)
            .attr("width", width)
            .attr("height", height)
            .attr("viewBox", [0, 0, width, height])
            .append("g")
            .call(g => g.append("text")
                .attr("x", width / 2)
                .attr("y", height / 2)
                .attr("fill", "black")
                .attr("font-size", "20px")
                .attr("text-anchor", "middle")
                .text("Too Few Types for TSNE"));
        svg.append("g")
            .call(g => g.append("text")
                .attr("x", width / 2)
                .attr("y", 20)
                .attr("fill", "black")
                .attr("font-size", "15px")
                .attr("text-anchor", "middle")
                .text(graph));
        return;
    }
    data = result.nodes;

    let x = d3.scaleLinear()
        .domain(d3.extent(data, d => d.x)).nice()
        .range([margin.left, width - margin.right])

    let y = d3.scaleLinear()
        .domain(d3.extent(data, d => d.y)).nice()
        .range([height - margin.bottom, margin.top])


    let xAxis = g => g
        .attr("transform", `translate(0,${height - margin.bottom})`)
        .call(d3.axisBottom(x).ticks(width / 80))
        .call(g => g.select(".domain").remove())
        .call(g => g.append("text")
            .attr("x", width)
            .attr("y", margin.bottom - 4)
            .attr("fill", "currentColor")
            .attr("text-anchor", "end")
            .text(data.x))

    let yAxis = g => g
        .attr("transform", `translate(${margin.left},0)`)
        .call(d3.axisLeft(y))
        .call(g => g.select(".domain").remove())
        .call(g => g.append("text")
            .attr("x", -margin.left)
            .attr("y", 10)
            .attr("fill", "currentColor")
            .attr("text-anchor", "start")
            .text(data.y))


    let grid = g => g
        .attr("stroke", "currentColor")
        .attr("stroke-opacity", 0.1)
        .call(g => g.append("g")
            .selectAll("line")
            .data(x.ticks())
            .join("line")
            .attr("x1", d => 0.5 + x(d))
            .attr("x2", d => 0.5 + x(d))
            .attr("y1", margin.top)
            .attr("y2", height - margin.bottom))
        .call(g => g.append("g")
            .selectAll("line")
            .data(y.ticks())
            .join("line")
            .attr("y1", d => 0.5 + y(d))
            .attr("y2", d => 0.5 + y(d))
            .attr("x1", margin.left)
            .attr("x2", width - margin.right));

    shape = d3.scaleOrdinal(data.map(d => d.group), d3.symbols.map(s => d3.symbol().type(s)()))

    const svg = d3.select(graph)
        .attr("width", width)
        .attr("height", height)
        .attr("viewBox", [0, 0, width, height])
        .append("g")
        .call(g => g.append("text")
            .attr("x", width / 2)
            .attr("y", 20)
            .attr("fill", "black")
            .attr("font-size", "15px")
            .attr("text-anchor", "middle")
            .text(graph));

    svg.append("g")
        .call(xAxis);

    svg.append("g")
        .call(yAxis);

    svg.append("g")
        .call(grid);

    svg.append("style")
        .text(`path.hidden { fill: #000; opacity: 0.1;}`);

    const node = svg.append("g")
        .attr("stroke-width", 1.5)
        .attr("font-family", "sans-serif")
        .attr("font-size", 5)
        .selectAll("path")
        .data(data)
        .join("path")
        .attr("transform", d => `translate(${x(d.x)},${y(d.y)})`)
        .attr("fill", d => d.id != subCenter ? color(d.group) : color(9))
        .attr("d", d => d.id != subCenter ? shape(d.group) : shape(9));

    node.append("title")
        .text(d => d.id);

    svg.on(".brush", null);

    svg.call(d3.brush()
        .on("start", brushstarted)
        .on("brush", brushed)
        .on("end", brushended));

    svg.selectAll(".selection")
        .attr("style", "display:none")
        .attr("x", null)
        .attr("y", null)
        .attr("width", null)
        .attr("height", null);

    svg.selectAll(".handle")
        .attr("style", "display:none")
        .attr("x", null)
        .attr("y", null)
        .attr("width", null)
        .attr("height", null);

    function brushstarted({ selection }) {

    }

    function brushed({ selection }) {
        if (!selection) return;
        const [[x0, y0], [x1, y1]] = selection;
        svg.selectAll("path").classed("hidden", d => {
            if (!d) return false;
            return x0 > x(d.x)
                || x1 < x(d.x)
                || y0 > y(d.y)
                || y1 < y(d.y);
        });
    }

    function brushended({ selection }) {
        if (!selection) return;
        const [[x0, y0], [x1, y1]] = selection;
        let selected_data = data.filter(d => {
            return x0 < x(d.x)
                && x1 > x(d.x)
                && y0 < y(d.y)
                && y1 > y(d.y);
        });
        console.log(selected_data);
        if (selected_data.length != 0) {
            selected_data.push(nodeMap[subCenter]);
            console.log(selected_data);
            table_filter(selected_data.map(d => d.id));
        }
        svg.selectAll("path").classed("hidden", false);
        svg.selectAll(".selection")
            .attr("style", "display:none")
            .attr("x", null)
            .attr("y", null)
            .attr("width", null)
            .attr("height", null);
        svg.selectAll(".handle")
            .attr("style", "display:none")
            .attr("x", null)
            .attr("y", null)
            .attr("width", null)
            .attr("height", null);

    }

    return svg;
}

var table_data;
async function table_layout(data) {
    console.log("table_data: ", data);
    var table = $('#relationTable').DataTable();
    table.clear();
    await getCenterRelation(data).then(function (response) {
        table_data = response.nodes
        // console.log(relation)
        table_data.forEach(function (d) {
            table.row.add([d.id, d.type, d.hops, d.relation.toFixed(3)]).draw(false);
        });
    });
    table.draw();
}

function table_filter(nodeIds) {
    var table = $('#relationTable').DataTable();
    table.clear();
    table_data.forEach(d => {
        if (nodeIds.includes(d.id)) {
            table.row.add([d.id, d.type, d.hops, d.relation.toFixed(3)]).draw(false);
        }
    })
    table.draw();
}

async function histgram_layout(data) {
    d3.selectAll("#histogram_layout > g").remove();
    data.center = subCenter;
    result = await getCenterRelation(data);
    data = result.nodes;

    const width = 600;
    const height = 225;
    const marginTop = 20;
    const marginRight = 20;
    const marginBottom = 30;
    const marginLeft = 40;

    // Bin the data.
    const bins = d3.bin()
        .thresholds(40)
        .value((d) => d.relation)(data);

    // Declare the x (horizontal position) scale.
    const x = d3.scaleLinear()
        .domain([bins[0].x0, bins[bins.length - 1].x1])
        .range([marginLeft, width - marginRight]);

    // Declare the y (vertical position) scale.
    const y = d3.scaleLinear()
        .domain([0, d3.max(bins, (d) => d.length)])
        .range([height - marginBottom, marginTop]);

    // Create the SVG container.
    const svg = d3.select("#histogram_layout")
        .attr("width", width)
        .attr("height", height)
        .attr("viewBox", [0, 0, width, height])
        .attr("style", "max-width: 100%; height: auto;")


    // Add a rect for each bin.
    svg.append("g")
        .call(g => g.append("text")
            .attr("x", -marginLeft + 50 + width / 2)
            .attr("y", 15)
            .attr("fill", "black")
            .attr("font-size", "15px")
            .attr("text-anchor", "middle")
            .text("Relation histogram"))
        .attr("fill", "steelblue")
        .selectAll()
        .data(bins)
        .join("rect")
        .attr("x", (d) => x(d.x0) + 1)
        .attr("width", (d) => x(d.x1) - x(d.x0) - 1)
        .attr("y", (d) => y(d.length))
        .attr("height", (d) => y(0) - y(d.length));


    // Add the x-axis and label.
    svg.append("g")
        .attr("transform", `translate(0,${height - marginBottom})`)
        .call(d3.axisBottom(x).ticks(width / 80).tickSizeOuter(0))
        .call((g) => g.append("text")
            .attr("x", width)
            .attr("y", marginBottom - 4)
            .attr("fill", "currentColor")
            .attr("text-anchor", "end")
            .text("Relation →"));

    // Add the y-axis and label, and remove the domain line.
    svg.append("g")
        .attr("transform", `translate(${marginLeft},0)`)
        .call(d3.axisLeft(y).ticks(height / 40))
        .call((g) => g.select(".domain").remove())
        .call((g) => g.append("text")
            .attr("x", -marginLeft)
            .attr("y", 10)
            .attr("fill", "currentColor")
            .attr("text-anchor", "start")
            .text("↑ Frequency (no. of nodes)"));

    // Return the SVG element.
    return svg.node();
}

function displayDetails(id) {
    var u = nodeMap[id];
    subCenter = id;
    var details = [];
    nodeLinks[id].forEach(function (d) {
        if (d.source == id) details.push(Object({ edge: d, neighbor: nodeMap[d.target] }));
        else details.push(Object({ edge: d, neighbor: nodeMap[d.source] }));
    });

    var khop_nodes = []; khop_nodes.push(u); u.hops = 0;
    // d3.select("#detail_info").selectAll("*").remove();
    var visited = []; visited.push(u.id);
    for (let i = 1; i <= hop; i++) {
        var next = [];
        var nextId = [];
        filteredData.links.forEach(function (d) {
            if (visited.includes(d.source) || visited.includes(d.target)) {
                if (!visited.includes(d.source) && !nextId.includes(d.source)) {
                    next.push(Object({ neighbor: nodeMap[d.source], value: d.value, dir: "in" }));
                    nextId.push(d.source);
                }
                if (!visited.includes(d.target) && !nextId.includes(d.target)) {
                    next.push(Object({ neighbor: nodeMap[d.target], value: d.value, dir: "out" }));
                    nextId.push(d.target);
                }
            }
        });
        visited = visited.concat(nextId);

        // print i-hop neighbors
        // d3.select("#detail_info").append("p")
        //     .text(i + "-hop neighbors:")
        //     .style("font-size", "23px");
        var neighbors = {};
        next.forEach(function (d) {
            var type = d.neighbor.type;
            if (!neighbors[type])
                neighbors[type] = [];
            neighbors[type].push(d);
        });
        // display detailed info
        // TODO: change into table layout
        // for (const type in neighbors) {
        //     neighbors[type].sort(function (a, b) { return a.value > b.value ? -1 : 1; })
        //     d3.select("#detail_info").append("p")
        //         .text(type + ": " + neighbors[type].reduce((acc, x) => acc + x.neighbor.id + (i == 1 ? (" (" + (x.dir == "in" ? "->" : "<-") + x.value.toFixed(2) + ")") : "") + " | ", acc = ""))
        //         .style("font-size", "23px");
        // }

        khop_nodes = khop_nodes.concat(next.map(d => {
            d.neighbor.hops = i;
            return d.neighbor;
        }));
        // using neighbor information to display k-hop barchart
        if (i == hop) {
            barchart_layout(neighbors, "#barchart_layout");
        }
    }

    var khop_edges = [];
    filteredData.links.forEach(function (d) {
        if (khop_nodes.includes(nodeMap[d.source]) && khop_nodes.includes(nodeMap[d.target]))
            khop_edges.push(d);
    });

    table_layout({ center: subCenter, nodes: khop_nodes, links: khop_edges });
    graph_layout({ nodes: khop_nodes, links: khop_edges }, "#sub_graph_layout", 300, 300, false);
    tsne_layout({ nodes: khop_nodes, links: khop_edges }, "#tsne_layout_node");
    tsne_layout({ nodes: khop_nodes, links: khop_edges }, "#tsne_layout_edge");
    histgram_layout({ nodes: khop_nodes, links: khop_edges })
    graph_layout(filteredData, "#main_graph_layout", 1100, 950, true);

    d3.select("#detail_header").selectAll("*").remove();
    d3.select("#detail_header").append("p")
        .text("Information for [" + u.type + ": " + u.id + "]")
        .style("font-size", "24px")
        .style("font-weight", "bold");
}

async function filter_data(data) {
    var target = document.getElementById("Search").value;
    if (target != "") {
        if (!totId.includes(target)) {
            alert("No such node!");
            return;
        }
    }

    var selectedNodes = [];
    var selectedEdges = [];

    d3.selectAll(".NodeType:checked").each(function (d) {
        selectedNodes.push(this.value);
    });
    d3.selectAll(".EdgeType:checked").each(function (d) {
        selectedEdges.push(this.value);
    });
    d3.selectAll(".Zoom:checked").each(function (d) {
        focus = this.value;
    });
    d3.selectAll(".Hop:checked").each(function (d) {
        hop = this.value;
    });

    await getFilteredData({ "nodeType": selectedNodes, "edgeType": selectedEdges }).then(function (response) {
        data = response;
    });

    data.nodes.forEach((d) => { groupId[d.type] = d.group; })

    filteredData = data;

    graph_layout(filteredData, "#main_graph_layout", 1100, 950, true);

    if (target != "") {
        subCenter = target;
        displayDetails(target);
    }
}

async function main() {
    var data;
    await get("./data/graph.json").then(function (response) {
        data = JSON.parse(response);
        // console.log(data)
    });

    data.nodes.forEach(function (d) { nodeMap[d.id] = d; totId.push(d.id); });
    data.links.forEach(function (e) {
        if (!nodeLinks[e.source]) nodeLinks[e.source] = [];
        if (!nodeLinks[e.target]) nodeLinks[e.target] = [];
        nodeLinks[e.source].push(e);
        nodeLinks[e.target].push(e);
    })

    filter_data({ ...data });
    d3.selectAll(".confirm_button").on("click", function () {
        filter_data({ ...data });
    });

    d3.selectAll(".Hop").on("change", function () {
        hop = this.value;
        if (subCenter != null) displayDetails(subCenter);
    });
    resize();

}

var s;
function resize() {
    s = window.screen.width / 2560;
    document.body.style.transformOrigin = '0 0';
    document.body.style.transform = 'scale(' + s + ',' + s + ')';
    document.body.style.width = window.innerWidth / s + 'px';
    document.body.style.height = window.innerHeight / s + 'px';
}
window.onresize = function () {
    resize();
}

main();